#pragma once

// 实现日志！！！

#include <iostream>
#include <string>
#include "Mutex.hpp"
#include <filesystem> //c++17才能用
#include <fstream>
#include <memory>
#include <sstream>
#include <unordered_map>
#include <ctime>
#include <cstdio>


namespace myLog{

    using namespace myMutex;

    // 日志的默认换行符
    const std::string sep = "\r\n";

    // 日志等级
    enum class LogLevel{
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
        //使用的时候需要自带前缀 LogLevel::
    };

    // 日志类型转字符串
    std::unordered_map<LogLevel, std::string> LogLevel2Str = {
        {LogLevel::DEBUG, "DEBUG"},
        {LogLevel::INFO, "INFO"},
        {LogLevel::WARNING, "WARNING"},
        {LogLevel::ERROR, "ERROR"},
        {LogLevel::FATAL, "FATAL"}
    };

    // 根据时间戳获得时间
    std::string GetCurrTime(){
        //使用localtime_r函数即可 这个函数是可以被重入的，即线程安全
        //struct tm *localtime_r(const time_t *timep, struct tm *result);

        //struct tm {
        //    int tm_sec;    /* Seconds (0-60) */
        //    int tm_min;    /* Minutes (0-59) */
        //    int tm_hour;   /* Hours (0-23) */
        //    int tm_mday;   /* Day of the month (1-31) */
        //    int tm_mon;    /* Month (0-11) */
        //    int tm_year;   /* Year - 1900 */
        //    int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
        //    int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
        //    int tm_isdst;  /* Daylight saving time */
        //};

        //先获得时间戳
        time_t current_time = time(nullptr);
        struct tm curr;
        localtime_r(&current_time, &curr);

        //这里的格式化想要达到效果使用c++是很困难的，直接用snprintf即可
        char time_buffer[128] = {0};

        // 在占位符前面加数字代表需要多少个位置标识，加0代表用0替代空位(如果有)
        snprintf(time_buffer, sizeof(time_buffer), "%04d_%02d_%02d %02d:%02d:%02d", 
        curr.tm_year + 1900, curr.tm_mon + 1, curr.tm_mday,
        curr.tm_hour, curr.tm_min, curr.tm_sec);

        return time_buffer;
    }

    // 先来实现策略模式 -> 即实现不同的策略来进行指定位置的输出
    // 采用的方法是 ： 继承 + 多态

    // 日志输出策略的类，这个让它变成抽象类（有纯虚函数），只定义方法
    class LogStrategy{
    public:
        LogStrategy() = default;
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string& message) = 0;
        // 不同模式核⼼是刷新方式的不同
    };

    // 输出到显示器上
    class DisplayStrategy : public LogStrategy{
    public:
        DisplayStrategy() = default;
        ~DisplayStrategy() = default;

        virtual void SyncLog(const std::string& message){
            Lock_Guard guard(_display_lock);
            std::cout << message << sep;
        }

    private:
        Mutex _display_lock;
    };

    // 输出到对应的文件上
    // 这里会有一个问题，就是文件需要[路径 + 文件名]
    // 所以，这里搞个默认的文件路径和文件名
    const std::string _file_path = "./LOG";
    const std::string _file_name = "my.log";
    
    class FileStrategy : public LogStrategy{
    public:
        FileStrategy(const std::string& path = _file_path, const std::string name = _file_name)
            :_path(path), _name(name)
        {
            // 这里有一个问题，就是文件的目录 文件不存在
            // 文件不存在不用怕，可以通过选项控制，不存在会自动创建
            // 但是，目录需要我们检查一下是否有打开

            // 防止同时创建
            Lock_Guard guard(_file_lock);

            if(std::filesystem::exists(_path)){
                //为真 -> 目录即存在
                return;
            }

            try{
                std::filesystem::create_directories(_path);
            }
            catch(std::filesystem::filesystem_error& error){
                std::cout << error.what() << std::endl; //这个就不打进日志里面去了
            }
        }

        virtual void SyncLog(const std::string& message){
            Lock_Guard guard(_file_lock);
            // 这里需要注意的是，文件路径 + “/” + 文件名
            // 但不确定中间的“/”是否存在，所以需要处理一下 
            std::string file_allname = _path 
            + ((_path.back() == '/' ) ? "" : "/")
            + _name;

            // 相当于打开file_allname文件，app选项是追加打开
            std::ofstream out(file_allname, std::ios::app);
            if(!out.is_open()) return;

            // 这里就可以把内容输出到指定的文件流
            // 可以使用out.write -> 但是比较麻烦
            // 直接使用 << 流插入运算符的重载即可！
            out << message << sep;

            out.close();
        }

        ~FileStrategy(){}

    private:
        std::string _path;
        std::string _name;
        Mutex _file_lock;
    };


  

    // 现在开始创建一个类Log，这个是用来选择日志的输出策略的
    class Log{
    public:
        Log(){
            // 默认打印到显示器
            DisplayEnable();
        }
        ~Log(){}

        // 选择策略 -> 提供给用户层选择
        void DisplayEnable(){
            _fflush = std::make_unique<DisplayStrategy>();
        }

        // 支持用户自行传递参数，当然，也可以不传，使用默认参数
        void FileEnable(const std::string& path = _file_path, const std::string name = _file_name){
            _fflush = std::make_unique<FileStrategy>(path, name);
        }


        // 定义一个内部类，用于处理信息 (为什么要用内部类下面说)
        class MessageFormat{
        public:
            // 把只能从外界传的内容写在构造内
            MessageFormat(LogLevel log_level, std::string filename, int line, Log& Logger)
                :_current_time(GetCurrTime()),   //把时间戳转化为字符串显示的时间，等下完善
                _Log_level(log_level),
                _pid(getpid()),
                _filename(filename), 
                _line(line),
                _logger(Logger)
            {
                //这里先把日志的左半部分给完成 使用string stream来进行格式化
                std::stringstream ss;
                // 作用就是把对应的内容以string的形式格式化
                ss << "[" << _current_time << "] "
                << "[" << LogLevel2Str[_Log_level] << "] " 
                << "[" << _pid << "] " 
                << "[" <<  _filename << "] " 
                << "[" << _line << "] " 
                << "- " ;

                _message = ss.str();

                // 但是，这里的loglevel本质上就是一个自定义类型，stringstream的流插入运算符肯定是没有重载的
                // 本质是整数，但是整数不够直观，所以需要有一个日志类型 -> 转化到对应字符串的方式
                // 这里直接用哈希表了
            }
            
            // 现在还需要处理日志的左半部分 如果是用c语言来写，就可使用可变参数
            // 但是，我们更希望的是，使用c++的流插入运算符
            // 这里需要使用一下模板参数，因为不知道传进来的是啥类型，如果是自定义类型(库内没重载的)，需要手动改一下
            template<class T>
            MessageFormat& operator<<(const T& message){
                std::stringstream ss;
                ss << message;
                _message += ss.str();
                // 返回这个值是为了能够连续地使用后续的 << ，就像连续赋值一样，要不然没办法追加使用 << 
                return *this;
            }

            ~MessageFormat(){
                if(_logger._fflush)
                    _logger._fflush->SyncLog(_message);
            }
            

        private:
            std::string _current_time; 
            LogLevel _Log_level;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _message; //要输出的信息
            Log& _logger;
        };


        // Log类()的重载，返回值应该是什么呢？
        // 这里需要知道，真正处理信息的是MessageFormat，但是做输出的是Log类
        // 应该怎么样让这两个类进行联动呢？

        //可以这样子:
        //让MessageFormat析构的时候，自动地就把内容给刷新出去即可！
        //刷新的就是使用那个全局的Log变量！
        //所以，这里的逻辑是：

        //外界选择策略，使用Log(LogLevel log_level, std::string filename, int line) << ...; 来进行输出日志
        //Log()调用的返回值是一个MessageFormat临时对象！生命周期只有一行
        //相当于是不断地向一个MessageFormat临时对象插入信息！
        //直到 << 插入信息结束后，该对象就会被析构！ -> 析构函数内直接刷新日志到指定位置
        //这是RAII思想！


        MessageFormat operator()(LogLevel log_level, std::string filename, int line){
            // 故意返回临时对象 ，就是为了达到上面的效果

            //其实结果都一样，因为外界调用的时候没有接收返回值，直接拿logger()进行使用，这本身就是一个临时变量！
            /* MessageFormat ms(log_level, filename, line, *this);
            return ms; */

            return MessageFormat(log_level, filename, line, *this);
        }

    private:
        // 底层是一个基类指针，上层控制一下指向哪一个基类，就可以输出到哪个地方
        std::unique_ptr<LogStrategy> _fflush;
    };


    

    //现在有一个问题，日志应该怎么使用？
    //真正给外界使用的是Log类，但是信息是在MessageFormat类内被格式化
    //应该怎么办呢？

    //还需要注意的一个问题是，由于所有的线程在输出日志的时候，肯定要用的是同一把锁！
    //直接定义一个全局的日志类 log logger！
    //然后通过logger(LogLevel log_level, std::string filename, int line) << ...... << .. ; 使用
    //所以，需要先给Log类实现()的重载

    //Log全局对象 -> 给用户层使用的
    Log logger;


// 为了方便用户层的使用，这里做一些宏替换
//使用宏，可以进行代码插⼊，方便随时获取文件名和⾏号
#define LOG(type) logger(type, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define ENABLE_Display_LOG_STRATEGY() logger.DisplayEnable()
#define ENABLE_FILE_LOG_STRATEGY() logger.FileEnable()
    
}




// 实现的日志形式 ： [时间] [日志等级] [发出日志进程的pid] [文件名] [行号] - 消息内容 支持可变参数

//时间通过时间戳可以转化

//日志等级:
/* DEBUG 
   INFO 
   WARNING 
   ERROR 
   FATAL */

//进程pid可以直接系统调用getpid

//文件名和行号可以用预处理 ： __FILE__  __LINE__

//消息内容等到内部设计的时候再说



// 还有就是需要可以选择输出到哪个地方 ： 文件 / 显示器

//日志功能：
//1.形成指定形式的日志消息
//2.通过策略模式，选择输出到的地方          